import os
import platform

from made.util import command, misc
from made.contrib import compilercommands

# TODO: If someone wants to put unrelated projects in separate subdirectories,
#       each using madefiles, and have a toplevel madefile to build them all,
#       we want the output directories to be part of the state, not global
#       variables.
# TODO: We also need a way to mark a madefile as a toplevel madefile so made
#       doesn't look in a parent directory and so runtime.toplevel_madefile
#       can work in the above scenario.
def initialize(
        dynamic_library_output_dir=None, static_library_output_dir=None,
        binary_output_dir=None, object_file_output_dir=None,
        import_lib_output_dir=None, version=None, cpp_compiler_chain=None):
    set_dynamic_library_output_dir(dynamic_library_output_dir)
    set_static_library_output_dir(static_library_output_dir)
    set_binary_output_dir(binary_output_dir)
    set_object_file_output_dir(object_file_output_dir)
    set_import_lib_output_dir(import_lib_output_dir)
    set_version(version)

    # Users can call set_cpp_compiler_chain to use a different compiler chain.
    if cpp_compiler_chain is None:
        cpp_compiler_chain = _create_cpp_compiler_chain()
    set_cpp_compiler_chain(cpp_compiler_chain)

cpp_compiler_chain = None
def set_cpp_compiler_chain(chain):
    global cpp_compiler_chain
    cpp_compiler_chain = chain

def _create_cpp_compiler_chain():
    return (
        compilercommands.MSVCCompilerChain() if platform.system() == "Windows"
        else compilercommands.GCCCompilerChain())

_dynamic_library_output_dir = None
def set_dynamic_library_output_dir(dir):
    global _dynamic_library_output_dir
    _dynamic_library_output_dir = dir

_static_library_output_dir = None
def set_static_library_output_dir(dir):
    global _static_library_output_dir
    _static_library_output_dir = dir

_binary_output_dir = None
def set_binary_output_dir(dir):
    global _binary_output_dir
    _binary_output_dir = dir

_object_file_output_dir = None
def set_object_file_output_dir(dir):
    global _object_file_output_dir
    _object_file_output_dir = dir

_import_lib_output_dir = None
def set_import_lib_output_dir(dir):
    global _import_lib_output_dir
    _import_lib_output_dir = dir

_version = None
def set_version(version):
    global _version
    _version = version

# TODO: We likely want this to be part of the state.
_input_dynamic_library_extensions = {}
def input_dynamic_library_extension():
    # First check if there is a platform-specific override for the active
    # compiler chain.  Then check if there is any override at all.  Otherwise
    # use the default extension.
    return (_input_dynamic_library_extensions.get(
            (cpp_compiler_chain.name, platform.system())) or
        _input_dynamic_library_extensions.get(cpp_compiler_chain.name) or
        cpp_compiler_chain.input_dynamic_library_default_extension())

def set_input_dynamic_library_extension_for_cpp_compiler_chain(
        extension, cpp_compiler_chain_name, platform=None):
    _input_dynamic_library_extensions[
        (cpp_compiler_chain_name if platform is None
            else (cpp_compiler_chain, platform))] = extension

# TODO: We likely want this to be part of the state.
_static_library_extensions = {}
def static_library_extension():
    # First check if there is a platform-specific override for the active
    # compiler chain.  Then check if there is any override at all.  Otherwise
    # use the default extension.
    return (_static_library_extensions.get(
            (cpp_compiler_chain.name, platform.system())) or
        _static_library_extensions.get(cpp_compiler_chain.name) or
        cpp_compiler_chain.static_library_default_extension())

def set_static_library_extension_for_cpp_compiler_chain(
        extension, cpp_compiler_chain_name, platform=None):
    _static_library_extensions[
        (cpp_compiler_chain_name if platform is None
            else (cpp_compiler_chain, platform))] = extension

class _State(object):
    _state_stack = []

    @classmethod
    def current_state(cls):
        return cls._state_stack[-1]

    def __init__(self):
        if len(self._state_stack) == 0:
            self._init_empty_state()
        else:
            self._copy_from_state(self.current_state())

        self._state_stack.append(self)
        command.push_environ()

    def _init_empty_state(self):
        self.defines = []
        self.include_dirs = []
        # TODO: We likely need separate dynamic and static link dirs lists.
        self.link_dirs = []
        self.link_libs = []
        self.object_files = []
        self.msvc_default_libs = []
        self.extra_options_dict = {}
        self.overridden_debug_options = None
        self.overridden_optimization_options = None

    def _copy_from_state(self, state):
        self.defines = state.defines[:]
        self.include_dirs = state.include_dirs[:]
        self.link_dirs = state.link_dirs[:]
        self.link_libs = state.link_libs[:]
        self.object_files = state.object_files[:]
        self.msvc_default_libs = state.msvc_default_libs[:]
        self.extra_options_dict = state.extra_options_dict.copy()
        self.overridden_debug_options = (
            None if state.overridden_debug_options is None
            else state.overridden_debug_options[:])
        self.overridden_optimization_options = (
            None if state.overridden_optimization_options is None
            else state.overridden_optimization_options[:])

    def add_extra_options(
            self, options, operation, is_debug=None, language=None):
        for is_debug, language in self._enumerate_debug_and_language_settings(
                is_debug, language):
            self.extra_options_dict.setdefault(
                (operation, is_debug, language), []).extend(options)

    def add_extra_option(self, option, operation, is_debug=None, language=None):
        self.add_extra_options((option,), operation, is_debug, language)

    def remove_extra_options(
            self, options, operation, is_debug=None, language=None):
        for is_debug, language in self._enumerate_debug_and_language_settings(
                is_debug, language):
            options_list = self.extra_options_dict.get(
                (operation, is_debug, language), [])

            for option in options:
                if option in options_list:
                    options_list.remove(option)

    def remove_extra_option(
            self, option, operation, is_debug=None, language=None):
        self.remove_extra_options((option,), operation, is_debug, language)

    def _enumerate_debug_and_language_settings(self, is_debug, language):
        is_debug_values = (
            (True, False) if is_debug is None else (is_debug,))
        language_values = (
            (compilercommands.language.c, compilercommands.language.cpp)
            if language is None else (language,))

        for is_debug in is_debug_values:
            for language in language_values:
                yield (is_debug, language)

    def set_environ_var(name, value=""):
        command.set_environ_var(name, value)

    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):
        command.pop_environ()
        self._state_stack.pop()

    @classmethod
    def _all_state_stack_values(cls, attrib_name, **kwargs):
        result = []
        for element in getattr(cls.current_state(), attrib_name):
            if callable(element):
                result.extend(element(**kwargs))
            else:
                result.append(element)
        return result

# Ensure the state stack is not empty.
_State()

def push_state():
    """The returned state can optionally be used in a with statement if you
    want to pop the state afterwards.
    """
    return _State()

def commands_to_compile(
        source_files, is_debug, library_name, binary_name, is_for_dynamic,
        output_subdir):
    assert(library_name is None or binary_name is None)
    kwargs = dict(
        is_shared_library=is_for_dynamic and library_name is not None,
        library_name=library_name,
        is_debug=is_debug)

    source_and_object_files = [
        (source_file, source_to_object_file(
            source_file, output_subdir=output_subdir,
            library_name=library_name, binary_name=binary_name))
        for source_file in source_files]

    return cpp_compiler_chain.compile_files(
        source_and_object_files,
        defines=_State._all_state_stack_values("defines", **kwargs),
        include_dirs=_State._all_state_stack_values("include_dirs", **kwargs),
        is_debug=is_debug,
        is_for_dynamic=is_for_dynamic,
        extra_options_dict=_State.current_state().extra_options_dict,
        overridden_debug_options=
            _State.current_state().overridden_debug_options,
        overridden_optimization_options=
            _State.current_state().overridden_optimization_options)

def commands_to_link(
        name, object_files, link_libraries, use_dynamic_libraries, is_library,
        is_debug, subsystem=None):
    output_file = link_output_file_path(name, use_dynamic_libraries, is_library)
    link_dirs = [_link_lib_input_dir(use_dynamic_libraries)]
    # TODO: We likely need separate dynamic and static link dirs.
    link_dirs.extend(_State._all_state_stack_values("link_dirs"))

    kwargs = {}
    if cpp_compiler_chain.name == "msvc":
        if not is_library or use_dynamic_libraries:
            kwargs["default_libs"] = _State._all_state_stack_values(
                "msvc_default_libs")

            if _version is not None:
                kwargs["version"] = _version

        if is_library and use_dynamic_libraries:
            kwargs["import_lib"] = "%s/%s%s" % (
                _import_lib_output_dir, name, input_dynamic_library_extension())

        if not is_library and subsystem is not None:
            kwargs["subsystem"] = subsystem
    else:
        if is_library and use_dynamic_libraries:
            kwargs["use_rpath"] = True
        if not is_library:
            kwargs["rpath_lib_dir"] = _link_lib_input_dir(is_for_dynamic=True)
            kwargs["rpath_bin_dir"] = os.path.dirname(output_file)

    all_link_libraries = list(link_libraries)
    all_link_libraries.extend(_State._all_state_stack_values("link_libs"))

    if is_library:
        if use_dynamic_libraries:
            return cpp_compiler_chain.link_dynamic_library(
                object_files, output_file, link_dirs, all_link_libraries,
                is_debug=is_debug,
                extra_options_dict=_State.current_state().extra_options_dict,
                **kwargs)

        return cpp_compiler_chain.link_static_library(
            object_files, output_file,
            extra_options_dict=_State.current_state().extra_options_dict,
            **kwargs)

    return cpp_compiler_chain.link_binary(
        object_files, output_file, link_dirs, all_link_libraries,
        is_debug=is_debug,
        extra_options_dict=_State.current_state().extra_options_dict,
        **kwargs)

def _library_output_dir(is_for_dynamic):
    return (
        _dynamic_library_output_dir if is_for_dynamic
        else _static_library_output_dir)

def _link_lib_input_dir(is_for_dynamic):
    return (_import_lib_output_dir
        if is_for_dynamic and cpp_compiler_chain.name == "msvc"
        else _library_output_dir(is_for_dynamic))

def link_output_file_path(name, use_dynamic_libraries, is_library):
    """Note that for dynamic libraries, in MSVC terminology, this function
    returns the .dll file, not the import library.
    """
    if is_library:
        library_directory = _library_output_dir(use_dynamic_libraries)
        extension = (cpp_compiler_chain.dynamic_library_extension()
            if use_dynamic_libraries else static_library_extension())
        return "%s/%s%s" % (library_directory, name, extension)

    return "%s/%s%s" % (
        _binary_output_dir, name, cpp_compiler_chain.executable_extension())

def link_input_file_path(name, use_dynamic_libraries):
    """With MSVC, this function returns the import library."""
    library_directory = _link_lib_input_dir(use_dynamic_libraries)
    extension = (input_dynamic_library_extension()
        if use_dynamic_libraries else static_library_extension())
    return "%s/%s%s" % (
        library_directory, name,
        (extension if not misc.extension(name) else ""))

def source_to_object_files(
        source_files, output_subdir=None, library_name=None, binary_name=None):
    return tuple(
        source_to_object_file(
            source_file, output_subdir=output_subdir,
            library_name=library_name, binary_name=binary_name)
        for source_file in source_files)

def source_to_object_file(
        source_file, output_subdir=None, library_name=None, binary_name=None):
    assert(not os.path.isabs(source_file))
    assert(library_name is None or binary_name is None)

    output_dir = (
        misc.join_with_slashes(
            _object_file_output_dir,
            ("lib" + library_name if library_name is not None else ""),
            (binary_name if binary_name is not None else ""),
            (output_subdir if output_subdir is not None else ""))
        if _object_file_output_dir is not None else output_subdir)

    return misc.join_with_slashes(
        (output_dir if output_dir is not None else ""),
        os.path.splitext(source_file)[0] + _object_extension())

def _object_extension():
    # TODO: Consider a different object extension for Windows.
    return ".o"
